Rust, a new frontier
The premise
In the past few years I've been slowly coming in contact with more and more Rust code. Whether it's packaging or debugging, it felt like it's about time to start learning it in more detail.
So my first project for doing this was porting over the bitte-cli from Crystal to Rust, and hoping I wouldn't completely get bogged down trying to appease the compiler.
Work is going on in the rust
branch, but it might be in master
by the time
you're reading this and everyone is happy with the new implementation.
Given that the code was already well-typed and working with Crystal, there wasn't a lot of mental work trying to figure out /how/ it should work. By simply focusing on translation I could focus on the patterns of Rust instead.
The code is really still rather ugly and inefficient, but since I prefer learning by doing over reading books, i simply started hacking using the awesome Emacs LSP and rust-analyzer combo. It took a bit of fiddling getting the analyzer to work, but after I figured out a working nix shell, it was a pleasure to have instance feedback on type and syntax errors and a bunch of more advanced refactoring utilities.
Here's a gist of that, but check out the code for the exact versions of nixpkgs, since it seems the requirements change rather often.
pkgs.mkShell {
RUST_SRC_PATH = pkgs.rustPlatform.rustLibSrc;
RUST_BACKTRACE = 1;
buildInputs = with pkgs; [
openssl
pkg-config
rustc
cargo
(rustracer.overrideAttrs (old: { checkPhase = null; }))
rust-analyzer
rustfmt
clippy
];
};
Now to the language itself. A lot has been written about it already, so I'm trying to keep this short.
The good
Tooling around the language is really good. About the same level as the Go
ecosystem. There is rustfmt
so you never have to worry about writing
well-formatted code. With rust-analyzer
you more or less write the code for
you via automated imports, auto completion, type hints, jumping between
definitions, and lots more.
The biggest difference to Go related to tooling is cargo
. It's a shame every
language ecosystem reinvents an ad hoc informally-specified, bug-ridden, slow
implementation of half of Nix. But at least this one has a rather nice lock file
with checksums.
It also helps that the Nix community has done a tremendous amount of work around packaging Rust applications, although the best option in nixpkgs now seems to be using FOD (fixed output derivation).
An alternative to this for many projects could be
haskell.nix, a fork of
Naersk by IOHK and able to do incremental compilation based on the hashes in the
Cargo.lock
itself. This gives a major boost in compilation speed, but doesn't
quite work with all Rust projects out there yet.
Community adoption is quite good with Rust. There are a large number of crates available for most things I needed so far.
Option/Result types all the way. This is a rather big annoyance with Crystal,
where there is often no indication of which things explode on you unless you
read the docs and use ?
-methods all over the place.
Having someone bark at you when you forget to deal with a result (but still
compiling) is IMHO the best compromise between the way Crystal and Go
handle those issues.
The bad
Finding good crates is a bit hard. Squatting nice names for crates seems quite common, so the name space is polluted and many projects settle for more obscure names.
It's not immediately obvious which crates use unsafe code, although there might be some way to find out, I haven't found it yet. For a language that touts its safety, this seems like something you should display prominently next to each project.
Build speed is, like a lot of compiled languages, still rather subpar. This is most likely related to the large number of dependencies you end up with for even seemingly simple projects, caused by the almost microscopically small standard library.
Speaking of standard library. If you're coming from well-endowed languages like
Ruby, Crystal, Go, or even JavaScript, Erlang and Python... you'll be in for a bit of a
shock. There aren't even regular expressions in the stdlib, never mind JSON or
HTTP handling. This makes it pretty hard to even start a project without
immediately having to add a ton of dependencies to it. bitte-cli
is currently
at nearly 300 crates, and it feels like a huge liability given that you're usually
supposed to take ownership of everything that goes into your result.
The ugly
Semicolons... semicolons everywhere! I know this is just my personal preference, and Nix got me a bit more accustomed to making sure they are where they need to be, but this is just not something I can understand for a language released way after many other languages have already proven that newlines are perfectly acceptable semicolons. See automatic semicolon insertion in Go) for example. Anyway, much has been said about this issue already, and I'm not bringing anything new to the table, but it's just a sad state of affairs.
The other side-effect of having a tiny stdlib is that there is no consensus around a common set of crates for basic functionality, and you end up with duplication of many parts of your stack.
For example I'm using restson
for talking with the Terraform API, but for the
AWS API I'm using a crate called rusoto
, which uses hyper
under the hood...
and suddenly my whole app has to also use tokio
and async/await
sprinkled
all over the code, even though async is nearly pointless in this context.
The strange
No post about Rust would be complete without mentioning the borrow checker. It's something that will bend your mind in novel ways, especially if you've so far mostly used garbage-collected languages.
I should probably start reading some more up on this, so far I've just been shifting things around until the compiler didn't yell at me anymore. But I recently learned that most of the things I'm doing are probably going to tank performance. Which isn't really an issue for a tiny CLI application, but will definitely have to think about it when doing something more intensive.